﻿namespace dogd
{
	using System;
	using System.Collections.Generic;
	using System.Collections.ObjectModel;
	using System.Data.Common;
	using System.Reflection;

	/// <summary>
	/// ResultReader reads single objects or lists of objects of type T from a repository.
	/// </summary>
	/// <typeparam name="T"></typeparam>
	public class ResultReader<T> where T : new()
	{
		/// <summary>
		/// Initialize with a data reader and metadata.
		/// </summary>
		/// <param name="reader"></param>
		/// <param name="metadata"></param>
		public ResultReader(DbDataReader reader, TableMetadata metadata)
		{
			this.Reader = reader;
			this.Metadata = metadata;
		}

		private DbDataReader Reader
		{
			get;
			set;
		}

		private TableMetadata Metadata
		{
			get;
			set;
		}

		/// <summary>
		/// Read the first instance from the repository. Throws CannotReadObjectException if no object is found.
		/// </summary>
		/// <returns></returns>
		public T ReadFirst()
		{
			if (this.Reader.Read())
			{
				PropertyNameTypePair[] properties = this.LoadProperties();

				T objectInstance = (T)Activator.CreateInstance(typeof(T));

				this.SetProperties(objectInstance, properties);

				return objectInstance;
			}

			throw new CannotReadObjectException("Cannot read object of type " + typeof(T).ToString());
		}

		/// <summary>
		/// Read all objects of type T from the repository and sets all public persisted properties.
		/// </summary>
		/// <returns></returns>
		public ICollection<T> Read()
		{
			List<T> objList = new List<T>();

			bool firstTime = true;
			PropertyNameTypePair[] properties = null;

			while (this.Reader.Read())
			{
				if (firstTime)
				{
					properties = this.LoadProperties();
					firstTime = false;
				}

				T objectInstance = (T)Activator.CreateInstance(typeof(T));

				this.SetProperties(objectInstance, properties);

				objList.Add(objectInstance);
			}

			return objList;
		}

		/// <summary>
		/// Reads a simple integer value.
		/// </summary>
		/// <param name="ordinal"></param>
		/// <returns></returns>
		public int ReadAsInt32(int ordinal)
		{
			int value = 0;

			if (this.Reader.Read())
			{
				value = this.Reader.GetInt32(ordinal);
			}

			return value;
		}

		private PropertyNameTypePair[] LoadProperties()
		{
			int fieldCount = this.Reader.FieldCount;

			PropertyNameTypePair[] properties = new PropertyNameTypePair[fieldCount];

			for (int columnIndex = 0; columnIndex < fieldCount; ++columnIndex)
			{
				string columnName = this.Reader.GetName(columnIndex);

				ColumnMetadata metaData = this.Metadata[columnName];

				properties[columnIndex] = new PropertyNameTypePair()
				{
					PropertyName = metaData.PropertyName,
					PropertyType = metaData.PropertyType
				};
			}

			return properties;
		}

		private void SetProperties(T instance, PropertyNameTypePair[] properties)
		{
			var instanceType = typeof(T);

			for (int propertyIndex = 0; propertyIndex < properties.Length; ++propertyIndex)
			{
				Type propertyType = properties[propertyIndex].PropertyType;

				if (!this.Reader.IsDBNull(propertyIndex))
				{
					object propertyValue = Convert.ChangeType(this.Reader.GetValue(propertyIndex), propertyType, System.Globalization.CultureInfo.InvariantCulture);

					var pi = instanceType.GetProperty(properties[propertyIndex].PropertyName);
					pi.SetValue(instance, propertyValue, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, null, null, null);
				}
			}
		}

		private class PropertyNameTypePair
		{
			public string PropertyName
			{
				get;
				set;
			}

			public Type PropertyType
			{
				get;
				set;
			}
		}
	}
}
